Registering CLR Aggregates
Similarly
to CLR functions, you must register your CLR aggregates with a SQL
Server database. First register the assembly containing the aggregate
using CREATE ASSEMBLY. Then, use the CREATE AGGREGATE statement with the EXTERNAL NAME parameter to register the aggregate. You must specify input parameter information and return type.
In Examples 8 and 9 we will register the SayHello function we have created earlier in this chapter. The SayHello function takes no parameters, and returns a value of type SqlString. The SqlString type is equivalent of the nvarchar SQL Server data type.
Example 8. Registering a Simple CLR Function
CREATE AGGREGATE ListWithSeparator (@value nvarchar(200)) RETURNS nvarchar(max) EXTERNAL NAME MyAwesomeFunctions.ListWithSeparator
|
Example 9. Calling a CLR Aggregate
USE AdventureWorks; SELECT dbo.ListWithSeparator(Person.Contact.EmailAddress) FROM Person.Contact WHERE Person.Contact.FirstName like 'Val%' --RESULTS (Truncated for brevity): ----------------------------------------------------------------------------- [email protected]; mailto:[email protected]; valerie2@ad... (1 row(s) affected)
|
Creating CLR Stored Procedures
Stored
procedures are similar to functions, except they are not used in
single-valued expressions. Stored procedures can return multiple result
sets, return error messages to the caller, and execute Data
Manipulation Language (DML) and Data Definition Language (DDL)
statements. In summary, stored procedures are designed to store
compiled logic that may manipulate data. Stored procedures can be
implemented using Transact-SQL and the CLR.
Creating
a CLR stored procedure is very similar to creating a CLR function.
Create a class, create a static/shared method, and mark it with the SqlProcedure
attribute. The difference is that a CLR stored procedure must be either
a void (in other words, return nothing) or return an integer value that
represents the success or failure code.
You can also return tabular results and messages by using the SqlContext.Pipe object. This object represents the output to the SQL Server caller, and has the methods of Send and Execute. Use Send to send text, and Execute to send a query that will be executed by the SQL Server caller.
CLR stored procedures are registered with the database using the CREATE PROCEDURE statement with the EXTERNAL NAME clause. As with function and aggregate registration, you must specify the parameter definitions and return value, if any.
Examples 10 through 12
demonstrate how to create, register, and call a CLR stored procedure.
In this stored procedure, we will write all scrap reasons from the
Production.ScrapReason table to a text file. The stored procedure will
accept the path to the text file as a parameter. We will call this
stored procedure—ExportComplaints.
Example 10. Implementing the ExportComplaints Stored Procedure — C#
using System; using System.Data; using System.Data.SqlClient; using System.Data.SqlTypes; using Microsoft.SqlServer.Server; using System.IO;
public class StoredProcedures { [Microsoft.SqlServer.Server.SqlProcedure()] public static void ExportComplaints(SqlString PathToLetter) { using (StreamWriter writer = new StreamWriter(PathToLetter. ToString())){ using (SqlConnection connection = new SqlConnection("context connection=true")) { connection.Open(); SqlCommand command = new SqlCommand("Select Name from Production. ScrapReason", connection); using (SqlDataReader reader = command.ExecuteReader()) { while (reader.Read()) { writer.WriteLine(reader[0].ToString()); } } } } SqlContext.Pipe.Send("Message from ExportComplaints CLR Stored Proc: Letter created successfully!"); } }
|
Example 11. Implementing the Export Complaints Stored Procedure—Visual Basic .NET
Imports System Imports System.Data Imports System.Data.SqlClient Imports System.Data.SqlTypes Imports Microsoft.SqlServer.Server Imports System.IO Public Class StoredProcedures <Microsoft.SqlServer.Server.SqlProcedure(DataAccess:=DataAccessKind.Read)> _ Public Shared Sub ExportComplaints(ByVal PathToLetter As SqlString) Using writer As StreamWriter = New StreamWriter(PathToLetter. ToString()) Using connection As SqlConnection = New SqlConnection("context connection=true") connection.Open() Dim command As SqlCommand = New SqlCommand("Select Name from Production.ScrapReason", connection) Using reader As SqlDataReader = command.ExecuteReader() Do While reader.Read() writer.WriteLine(reader(0).ToString()) Loop End Using End Using End Using SqlContext.Pipe.Send("Message from ExportComplaints CLR Stored Proc: Letter created successfully!") End Sub End Class
|
Example 12. Registering the Export Complaints Stored Procedure with SQL Server
CREATE ASSEMBLY MyAwesomeFunctions AUTHORIZATION [dbo] FROM 'C:\Documents and Settings\Valentine\My Documents\MyAwesomeFunctions\bin\ MyAwesomeFunctions.dll' WITH PERMISSION_SET = EXTERNAL_ACCESS; Go CREATE PROCEDURE dbo.ExportComplaints(@PathToLetter as nvarchar(1000)) AS EXTERNAL NAME MyAwesomeFunctions.StoredProcedures.ExportComplaints; Go
|
Note
that this time we had to register the assembly with the EXTERNAL_ACCESS
permission set; otherwise, we would not be able to write to the text
file.
Example 13 shows the syntax for executing the ExportComplaints stored procedure.
Example 13. Executing the ExportComplaints Stored Procedure
EXECUTE dbo.ExportComplaints 'C:\Documents and Settings\Valentine\ My Documents\Complaint.txt'
|
After
executing the stored procedure, a text file Complaint.txt will appear
in My Documents. If you have time, try performing this example
yourself—you will feel more comfortable with CLR integration and .NET
Framework development as a whole.
Tip
You
should understand what happens when an assembly that is registered with
the SAFE permission set attempts to access external resources. The
assembly code will receive an error. If we used the SAFE permission set
with the preceding ExportComplaints example, we would receive an error that looks like this:
A .NET Framework error occurred during execution of user-defined routine
or aggregate "ExportComplaints": System.Security.SecurityException:
Request for the permission of type 'System.Security.Permissions.
FileIOPermission, mscorlib, Version=2.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089' failed.
Creating CLR Triggers
A
trigger is a statement that runs every time table data or the structure
of a database is modified. Data Manipulation Language (DML) triggers
run when someone runs an INSERT, UPDATE, or DELETE statement against a particular table. These triggers can
be used to perform complex validation and roll back the operation if
necessary. Data Definition Language (DDL) triggers run when someone
modifies the structure of a database, usually by executing CREATE, ALTER, and DROP statements. These triggers can be used to audit modifications to the structure of the database.
CLR triggers have the same capabilities as Transact-SQL triggers. Specifically, CLR triggers can perform the following tasks:
Determine which columns in a table have been updated
Query the INSERTED and DELETED tables to find out what data is about to be inserted, deleted, or modified
Determine which database objects were affected by a DDL statement
Most of these capabilities are accessed by using the SqlContext. TriggerContext
object. Using this object is outside the scope of this exam. However,
when you need to create a CLR trigger in real life, see SQL Server
Books Online.
CLR
triggers need to be registered with the specific table, just like
regular Transact-SQL triggers. To do this, first register the assembly
containing the trigger using CREATE ASSEMBLY, as explained earlier in this chapter. Then, use the CREATE TRIGGER statement to bind the trigger to the specific table, as shown in Example 14.
Example 14. Binding a CLR Trigger
CREATE TRIGGER dbo.ValidateCustomerEMailAddress ON Customers FOR INSERT, UPDATE AS EXTERNAL NAME MyAwesomeFunctions. Triggers.ValidateCustomerEMailAddress
|
Creating User-Defined Data Types Using the CLR
Ever
since SQL Server 2005, we have had the ability to create custom data
types and register them with SQL Server. These are known as
User-Defined Types, or UDTs. UDTs are often used to represent complex
data, such as spatial coordinates or currencies. Note that you should
not use UDTs to represent logical structures like customers or orders.
Complex structures are better implemented using traditional columns,
rows, and relationships. Don’t use UDTs just because you have the
ability. Always consider traditional ways to store your data as this
will deliver better performance. UDTs are created using managed code
and can be used in place of any SQL Server data type—for example, to
define columns in a table, to declare variables, and so on.
Creating
a UDT in managed code is similar to creating a user-defined aggregate.
Specifically, you create a class or a structure and mark it with the SqlUserDefinedType
custom attribute. You must then implement a user-defined type contract,
again, similar to creating a user-defined aggregate. Defining a CLR UDT
is outside the scope of this exam. However, when you need to create a
CLR UDT in real life, see SQL Server Books Online.
CLR
user-defined data types need to be registered before they can be used.
As with all CLR objects, you should first register the assembly with
SQL Server using the CREATE ASSEMBLY statement. You can then use the CREATE TYPE
statement to register the type with your database. The type is
registered with a single database, not with the entire instance of SQL
Server. This is another reason why you should consider alternatives to
CLR UDTs—cross-database and cross-server operations may be difficult or
even impossible. Similar to declaring a trigger, you should use the EXTERNAL NAME keywords to bind your UDT to the CLR object, as shown in Example 4.16.
Example 15. Registering a CLR User-Defined Data Type
CREATE TYPE dbo.RGBColor EXTERNAL NAME MyAwesomeFunctions.[RGBColor];
|
Exercise 1. Start to Finish: Implementing a CLR Function in SQL Server
In
this exercise, we will create a simple CLR function to read data from
the Windows registry. The function will accept a registry key name and
a value name. The function will return the data held in the registry
value. We will then register the function and call it from the SQL
Server Management Studio.
Before you begin, you must have the following software installed on your computer:
SQL Server 2008 (a free trial is available for download). Visual Studio 2005 or Visual Studio 2008. If you don’t have Visual Studio, download and install the .NET Framework 2.0 SDK. AdventureWorks sample database.
First we will create the CLR function. To do this, use the following procedure:
If you have Visual Studio installed, open Visual Studio. To do this click Start | All Programs | Microsoft Visual Studio 2005 | Microsoft Visual Studio 2005. In Visual Studio, on the menu bar, click File | New | Project. In the New Project dialog, in the left pane, expand either C# or Visual Basic (choose which language you wish to use). Under your language of choice click Database. In the Templates pane on the right, click SQL Server Project. In the Name box type MySQLLibrary, and choose to save the project to an easy-to-remember location. In this example we will use C:\Exercises. Click OK. In the Add Database Reference dialog, click Cancel. In the Solution Explorer window on the left-hand side, right-click MySQLLibrary, then click Add | User-Defined Function. In the Name box, type ReadRegistryData, then click OK.
The
large code window in the middle of the screen shows a template for a
user-defined function. You are ready to write the code for the
function. Copy the code shown in Examples 16 and 17
into your template, according to the language you have chosen. If you
do not use Visual Studio, simply open Notepad and copy the code into
the blank text file.
|
Example 16. Exercise Code—C#
using System; using System.Data; using System.Data.SqlClient; using System.Data.SqlTypes; using Microsoft.SqlServer.Server; using Microsoft.Win32; public partial class UserDefinedFunctions { [Microsoft.SqlServer.Server.SqlFunction] public static SqlString ReadRegistryData(SqlString Key, SqlString Value) { string hive = Key.ToString().ToUpper(); hive = hive.Substring(0, hive.IndexOf('\\')); string subKey = Key.ToString().Remove(0, hive.Length + 1); if (hive == "HKEY_LOCAL_MACHINE") { RegistryKey registryKey = Registry.LocalMachine. OpenSubKey(subKey, false); return (string)registryKey.GetValue(Value.ToString()); } else if (hive == "HKEY_CURRENT_USER") { RegistryKey registryKey = Registry.CurrentUser.OpenSubKey(subKey, false); return (string)registryKey.GetValue(Value.ToString()); } return "Reading this key is not supported."; } }
|
Example 17. Exercise Code—Visual Basic
Imports System Imports System.Data Imports System.Data.SqlClient Imports System.Data.SqlTypes Imports Microsoft.SqlServer.Server Imports Microsoft.Win32
Partial Public Class UserDefinedFunctions <Microsoft.SqlServer.Server.SqlFunction()> _ Public Shared Function ReadRegistryData(ByVal Key As SqlString, _ ByVal Value As SqlString) AsSqlString Dim hive As String = Key.ToString().ToUpper() hive = hive.Substring(0, hive.IndexOf("\")) Dim subKey As String = Key.ToString().Remove(0, hive.Length + 1) If hive = "HKEY_LOCAL_MACHINE" Then Dim keyToRead As RegistryKey = Registry.LocalMachine. OpenSubKey(subKey, False) Return CType(keyToRead.GetValue(Value.ToString()), String) ElseIf hive = "HKEY_CURRENT_USER" Then Dim keyToRead As RegistryKey = Registry.CurrentUser. OpenSubKey(subKey, False) Return CType(keyToRead.GetValue(Value.ToString()), String) End If Return "Reading this key is not supported." End Function End Class
|
If you are not using Visual Studio and have entered the code into Notepad, save the file to the C:\Exercises\MySQLLibrary\ folder. Name the file ReadRegistryData.vb if you are using Visual Basic, or ReadRegistryData.cs if you are using C#. You can choose any other folder, but be consistent throughout the exercise.
Now we are ready to compile our code into an assembly. To do this, use the following procedure:
If you are using Visual Studio, on the menu bar click Build | Build Solution. The assembly called MySQLLibrary.dll will be output to C:\Exercises\MySQLLibrary\MySQLLibrary\bin\Debug\MySQLLibrary.dll.
If
you are not using Visual Studio, open the command prompt and navigate
to the installation directory for .NET Framework (usually
C:\WINDOWS\Microsoft.NET\Framework\v2.0.xxxxx). Execute the following
command:
Compile—C#
csc /target:library /out:C:\Exercises\MySQLLibrary\MySQLLibrary.dll /debug
C:\Exercises\MySQLLibrary\ReadRegistryData.cs
Compile—Visual Basic .NET
vbc /target:library /out:C:\Exercises\MySQLLibrary\MySQLLibrary.dll /debug
C:\Exercises\MySQLLibrary\ReadRegistryData.vb
Now
that our code has been compiled to an assembly, we can register it with
SQL Server. Open SQL Management Studio, and execute the query shown in Example 18 against the AdventureWorks database.
Example 18. Creating the Assembly and Registering the ReadRegistryData Function in SQL Server
-- COMMENT: ENABLE CLR ON THE SERVER sp_configure 'show advanced options', 1; GO RECONFIGURE; GO sp_configure 'clr enabled', 1; GO RECONFIGURE; GO -- COMMENT: SET THE TRUSTWORTHY PROPERTY ALTER DATABASE AdventureWorks SET TRUSTWORTHY ON GO -- COMMENT: REGISTER THE ASSEMBLY CREATE ASSEMBLY MySQLLibrary AUTHORIZATION [dbo] FROM 'C:\Exercises\MySQLLibrary\MySQLLibrary.dll' WITH PERMISSION_SET = EXTERNAL_ACCESS; GO -- COMMENT: REGISTER THE FUNCTION CREATE FUNCTION dbo.ReadRegistryData(@Key as nvarchar(1000), @Value as nvarchar(200)) RETURNS nvarchar(max) AS EXTERNAL NAME MySQLLibrary.UserDefinedFunctions.ReadRegistryData; go
|
Finally, we are ready to test the function execution. Run the following statements and observe the results:
SELECT dbo.ReadRegistryData(
'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion',
'ProgramFilesDir')
SELECT dbo.ReadRegistryData(
'HKEY_CURRENT_USER\Control Panel\Desktop', 'Wallpaper')
SELECT dbo.ReadRegistryData(
'HKEY_CURRENT_USER\Control Panel\Current', 'Color Schemes')